"""publish 报文封装"""
from umq.errors import MQTTException, UMQException
from umq.adapters import StreamReaderAdapter
from umq.codecs import (read_or_raise, decode_string, decode_packet_id,
                        encode_string, int_to_bytes)
from umq.mqtt.packet import (PUBLISH, MqttFixedHeader,
                             MqttVariableHeader, MqttPayload, MqttPacket)


class PublishVariableHeader(MqttVariableHeader):
    """publish variable封装"""
    __slots__ = ('topic_name', 'packet_id')

    def __init__(self, topic_name: str, packet_id: int = None):
        super().__init__()

        # * 不能出现在publish的topic中
        if '*' in topic_name:
            raise MQTTException('[MQTT-3.3.2-2] Topic name in the PUBLISH Packet\
                                MUST NOT contain wildcard characters.')
        self.topic_name = topic_name
        self.packet_id = packet_id

    def __repr__(self):
        return type(self).__name__ + '(topic_name={}, packet_id={})'.format(
            self.topic_name, self.packet_id
        )

    @classmethod
    async def from_stream(cls, reader: StreamReaderAdapter, fixed: MqttFixedHeader):
        topic_name = await decode_string(reader)
        has_qos = (fixed.flags >> 1) & 0x03
        if has_qos:
            packet_id = await decode_packet_id(reader)
        else:
            packet_id = None

        return cls(topic_name, packet_id)

    def to_bytes(self):
        out = bytearray()
        out.extend(encode_string(self.topic_name))
        if self.packet_id is not None:
            out.extend(int_to_bytes(self.packet_id, 2))
        return out


class PublishPayload(MqttPayload):
    """publish payload 封装"""
    __slots__ = ('data', )

    def __init__(self, data: bytes = None):
        super().__init__()
        self.data = data

    def __repr__(self):
        return type(self).__name__ + '(data={0!r})'.format(repr(self.data))

    @classmethod
    async def from_stream(cls, reader: StreamReaderAdapter, fixed: MqttFixedHeader,
                          variable: PublishVariableHeader):
        data = bytearray()
        data_length = fixed.remaining_length - len(variable.to_bytes())
        length_read = 0
        while length_read < data_length:
            buf = await reader.read(data_length - length_read)
            data.extend(buf)
            length_read = len(data)

        return cls(data)


class PublishPacket(MqttPacket):
    """publish 封装"""

    DUP_FLAG = 0x80
    RETAIN_FLAG = 0x01
    QOS_FLAG = 0x06

    def __init__(self, fixed: MqttFixedHeader = None,
                 variable: PublishVariableHeader = None, payload: PublishPayload = None):
        if fixed is None:
            header = MqttFixedHeader(PUBLISH, 0x00)
        else:
            if fixed.packet_type is not PUBLISH:
                raise UMQException('Invalid packet type: %s' % fixed.packet_type)
            header = fixed
        super().__init__(header)
        self.variable = variable
        self.payload = payload

    def _set_header_flag(self, val, mask):
        if val:
            self.fixed_header.flags |= mask
        else:
            self.fixed_header.flags &= ~mask

    def _get_header_flag(self, mask):
        return True if self.fixed_header.flags & mask else False

    @property
    def dup_flag(self) -> bool:
        return self._get_header_flag(self.DUP_FLAG)

    @dup_flag.setter
    def dup_flag(self, val):
        self._set_header_flag(val, self.DUP_FLAG)

    @property
    def retain_flag(self) -> bool:
        return self._get_header_flag(self.RETAIN_FLAG)

    @retain_flag.setter
    def retain_flag(self, val):
        self._set_header_flag(val, self.RETAIN_FLAG)

    @property
    def qos(self):
        return (self.fixed_header.flags & self.QOS_FLAG) >> 1

    @qos.setter
    def qos(self, val: int):
        self.fixed_header.flags &= 0xF9
        self.fixed_header.flags &= (val << 1)

    @property
    def packet_id(self):
        return self.variable.packet_id

    @packet_id.setter
    def packet_id(self, val: int):
        self.variable.packet_id = val

    @property
    def data(self):
        return self.payload.data

    @data.setter
    def data(self, val: bytes):
        self.payload.data = val

    @property
    def topic_name(self):
        return self.variable.topic_name

    @topic_name.setter
    def topic_name(self, val: str):
        self.variable.topic_name = val

    @classmethod
    def build(cls, topic_name: str, message: bytes,
              packet_id: int, dup_flag, qos, retain):
        variable = PublishVariableHeader(topic_name, packet_id)
        payload = PublishPayload(message)
        packet = PublishPacket(variable=variable, payload=payload)
        packet.dup_flag = dup_flag
        packet.qos = qos
        packet.retain_flag = retain
        return packet
